package parsing;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;

import scanning.NumberToken;
import ast.ExactComplex;
import ast.InexactComplex;
import ast.NumberConstant;

public class NumberParser {

	private class ExactRational {

		ExactRational() {
		}

		ExactRational(String numerator, String denominator) {
			this.numerator = new BigInteger(numerator);
			this.denominator = new BigInteger(denominator);
		}

		BigInteger numerator;
		BigInteger denominator;

	}

	public NumberConstant parse(NumberToken token) {
		if (isExact(token))
			return parseExactComplex(token);
		else
			return parseInexactComplex(token);
	}

	private boolean isExact(NumberToken token) {
		if (token.hasExactPrefix())
			return true;
		if (token.hasInexactPrefix())
			return false;
		return inferredExactness(token);
	}

	private boolean inferredExactness(NumberToken token) {
		return isExact(token.getRealText(), token.getRadix())
				&& isExact(token.getImagText(), token.getRadix());
	}

	private boolean isExact(String text, int radix) {
		return !(isDecimal(text, radix) || text.contains("#") || text
				.startsWith("@"));
	}

	private boolean isDecimal(String text, int radix) {
		return text.contains(".")
				|| (radix == 10 && containsExponentMarker(text));
	}

	private boolean containsExponentMarker(String text) {
		return text.contains("e") || text.contains("s") || text.contains("f")
				|| text.contains("d") || text.contains("l");
	}

	private NumberConstant parseExactComplex(NumberToken token) {
		if (token.getImagText().startsWith("@"))
			throw new PolarComplexCannotBeExact();
		String realText = replaceHashesWithZeros(token.getRealText());
		String imagText = replaceHashesWithZeros(token.getImagText());
		int radix = token.getRadix();
		ExactRational real = parseExactReal(realText, radix);
		ExactRational imag = parseExactImag(imagText, radix);
		return new ExactComplex(real.numerator, real.denominator,
				imag.numerator, imag.denominator);
	}

	@SuppressWarnings("serial")
	public static class PolarComplexCannotBeExact extends RuntimeException {
	}

	private String replaceHashesWithZeros(String value) {
		return value.replaceAll("#", "0");
	}

	private ExactRational parseExactReal(String value, int radix) {
		if (value.isEmpty())
			return new ExactRational("0", "1");
		if (isRationalNumber(value))
			return parseExactRational(value, radix);
		if (isDecimal(value, radix))
			return parseExactDecimal(replaceExponentMarkerWithE(value));
		return parseExactInteger(value, radix);
	}

	private ExactRational parseExactImag(String value, int radix) {
		if (value.isEmpty())
			return new ExactRational("0", "1");
		if (value.equals("+i"))
			return new ExactRational("1", "1");
		if (value.equals("-i"))
			return new ExactRational("-1", "1");
		return parseExactReal(stripI(value), radix);
	}

	private String stripI(String value) {
		if (value.endsWith("i"))
			return value.substring(0, value.length() - 1);
		return value;
	}

	private boolean isRationalNumber(String realText) {
		return realText.contains("/");
	}

	private ExactRational parseExactRational(String value, int radix) {
		ExactRational result = new ExactRational();
		String[] rationalParts = value.split("/");
		result.numerator = new BigInteger(rationalParts[0], radix);
		result.denominator = new BigInteger(rationalParts[1], radix);
		return result;
	}

	private ExactRational parseExactInteger(String value, int radix) {
		ExactRational result = new ExactRational();
		result.numerator = new BigInteger(value, radix);
		result.denominator = new BigInteger("1");
		return result;
	}

	private ExactRational parseExactDecimal(String value) {
		ExactRational result = new ExactRational();
		result.numerator = createNumerator(value);
		result.denominator = createDenominator(value);
		return result;
	}

	private BigInteger createNumerator(String value) {
		return new BigInteger(stripExponent(value).replace(".", ""));
	}

	private String stripExponent(String value) {
		int numberEnd = value.indexOf('e');
		if (numberEnd != -1)
			return value.substring(0, numberEnd);
		return value;
	}

	private BigInteger createDenominator(String value) {
		int zeros = getDecimalOffset(value);
		StringBuilder denominator = new StringBuilder("1");
		for (int i = 0; i < zeros; i++)
			denominator.append("0");
		return new BigInteger(denominator.toString());
	}

	private int getDecimalOffset(String value) {
		int zeros = getDecimalOffsetFromExponent(value);
		value = stripExponent(value);
		zeros += value.length() - value.indexOf('.') - 1;
		return zeros;
	}

	private int getDecimalOffsetFromExponent(String value) {
		int numberEnd = value.indexOf('e');
		if (numberEnd != -1)
			return -Integer.parseInt(value.substring(numberEnd + 1));
		return 0;
	}

	private NumberConstant parseInexactComplex(NumberToken token) {
		BigDecimal real = parseInexactRealPart(token);
		BigDecimal imag = parseInexactImagPart(token);
		if (token.getImagText().startsWith("@"))
			return polarToRectangular(real, imag);
		return new InexactComplex(real, imag);
	}

	private BigDecimal parseInexactRealPart(NumberToken token) {
		String realText = replaceHashesWithZeros(token.getRealText());
		if (isRationalNumber(realText))
			return parseInexactRational(token, realText);
		return new BigDecimal(replaceExponentMarkerWithE(realText));
	}

	private String replaceExponentMarkerWithE(String realText) {
		return realText.replace('s', 'e').replace('f', 'e').replace('d', 'e')
				.replace('l', 'e');
	}

	private BigDecimal parseInexactRational(NumberToken token, String realText) {
		ExactRational exact = parseExactRational(realText, token.getRadix());
		BigDecimal numerator = new BigDecimal(exact.numerator);
		BigDecimal denominator = new BigDecimal(exact.denominator);
		return numerator.divide(denominator, MathContext.DECIMAL128);
	}

	private BigDecimal parseInexactImagPart(NumberToken token) {
		if (token.getImagText().isEmpty())
			return new BigDecimal("0");
		String value = replaceHashesWithZeros(token.getImagText());
		value = stripI(value);
		value = stripAtSign(value);
		if (isRationalNumber(value))
			return parseInexactRational(token, value);
		return new BigDecimal(replaceExponentMarkerWithE(value));
	}

	private String stripAtSign(String value) {
		if (value.startsWith("@"))
			return value.substring(1);
		return value;
	}

	private NumberConstant polarToRectangular(BigDecimal magnitude,
			BigDecimal angle) {
		double m = magnitude.doubleValue();
		double a = angle.doubleValue();
		BigDecimal real = BigDecimal.valueOf(m * Math.cos(a));
		BigDecimal imag = BigDecimal.valueOf(m * Math.sin(a));
		return new InexactComplex(real, imag);
	}
}
